/**
 * @file Define the application context class 
 * 
 * @author Steve Gao<gaofu@parllay.com>
 */
const path        = require('path');
const async       = require('async');
const fs          = require('fs');
const redis       = require('redis');
const mysql       = require('mysql');
const genericPool = require('generic-pool');
const jssha       = require('jssha');
const qiniu       = require('node-qiniu');
const alipay      = require('direct-alipay');
const wechat      = require('./apis/wechat-api');
const utils       = require('./utils');

const BaseService       = require('./services/base-service').BaseService;
const LogService        = require('./services/log-service').LogService;

const ApplicationError  = require('./errors').ApplicationError;

const {Configuration, SystemServices, CriticalErrors} = require('./constants');

/**
 * The Application Context, This class is used for preparing execution context
 * 
 * @class ApplicationContext
 * @param {Application} application - The OnWall Application
 * @param {String} cwd - The current work directory
 */
class ApplicationContext {

    get logger() {
        let logService = this.getSystemService(SystemServices.LOG_SERVICE);
        return this._logger = logService ? logService.getLogger() : console;
    }

    get cwd() {
        return this._cwd;
    }

    get Application() {
        return this._Application;
    }

    constructor(application, cwd) {
        this._Application = application;
        this._cwd = cwd;
        this.loadConfig();
        this.setupInternalServices();
        this.setupRedisPool();
        this.setupMySQLPool();
        this.setupQiniuClient();
        this.setupAlipay();
    }

    getCurrentWorkDirectory() {
        return this.cwd;
    }

    /**
     * Setup the internal services
     * 
     * @name setupInternalService
     * 
     * @returns {void}
     */
    setupInternalServices() {
        this.registerSystemService(SystemServices.LOG_SERVICE, LogService);
    }

    /**
     * The map for storing the service instances
     */
    get servicesMap() {
        if(!this._services) {
            this._services = new Map();
        }

        return this._services;
    }

    /**
     * Register system service into application context
     * 
     * @name registerSystemService
     * @param {String} name - The name of service in application context
     * @param {Class} serviceClass - The class of service
     * 
     * @returns {Boolean}
     */
    registerSystemService(name, serviceClass) {
        var className = serviceClass ? (serviceClass.name ? serviceClass.name : "Unknown Service") : "Unknown Service";
        try {
            var serviceInstance = new serviceClass(this);
            if (serviceInstance && !(serviceInstance instanceof BaseService)) {
                this.logger.error(`Failed to register system service(${className}), it is not a supported service.`);
                return false;
            }
            
            this.logger.info(`Registering system service(${className}) into application context ...`);
            this.servicesMap.set(name, serviceInstance);
            this.logger.info(`Registered system service(${className}) into application context.`)
        } catch( error ) {
            this.logger.error(`Error happend when loading service (${className})`, error);
        }

        return true;
    }

    /**
     * Get system service by name from application context
     * 
     * @name getSystemService
     * @param {String} name - The name of service in application context
     * 
     * @returns {void}
     */
    getSystemService(name) {
        if(this.servicesMap.get(name)) {
            return this.servicesMap.get(name);
        }

        return null;
    }

    get config() {
        return this._config;
    }

    /**
     * Get configuration by name
     * 
     * @name getConfiguration
     * @param {String} configName - The name of configuration item
     * 
     * @returns {any}
     */
    getConfiguration(configName) {
        if (this.config && this.config[configName]) {
            return this.config[configName];
        }

        return null;
    }

    /**
     * Load system configuration into context
     */
    loadConfig() {
        var NODE_ENV = process.env.NODE_ENV || 'dev';

        var configFile = path.resolve(this.cwd, `config/config-${NODE_ENV}.json`);
        if(fs.existsSync(configFile)) {
            var data = fs.readFileSync(configFile);
            this._config = JSON.parse(data);
        } else {
            throw new ApplicationError("Can not find the config file!", CriticalErrors.NOT_FOUND_CONFIG_FILE);
        }
    }


    get redisPool() {
        return this._redisPool;
    }

    /**
     * Setup the Redis connection pool
     */
    setupRedisPool() {
        var config = this.getConfiguration(Configuration.REDIS);
        var me = this;
        if(config) {
            me._redisPool = genericPool.Pool({
                name: "redis",
                create: function(callback) {
                    var client = redis.createClient(config.port, config.host, config.options);
                    if(config.password) {
                        client.auth(config.password);
                    }
                    client.on('error', function(error) {
                        //TODO: Add log here
                        me.redisPool.destroy(client);
                    });
                    callback(null, client);
                },
                destroy: function(client) {
                    client.quit();
                },
                max: config.max,
                min: config.min,
                idleTimeoutMillis: config.idleTimeoutMillis,
                reapIntervalMillis: config.reapIntervalMillis
            });
        }
    }

    /**
     * Acquire Redis Client from Redis connection pool
     * 
     * @name acquireRedisClient
     * @param {Function} parentCallback - The callback function
     * 
     * @returns {void}
     */
    acquireRedisClient(callback) {
        var pool = this.redisPool;
		if(pool) {
			pool.acquire(callback);
		} else {
			//this.getLogger().error('No redis connection pool for ' + poolName);
            //TODO: Add log here
			callback(new Error('No redis connection pool'), null);
		}
    }

    releaseRedisClient(client) {
		var pool = this.redisPool;
		if(pool) {
			pool.release(client);
		} else {
            //TODO: Add log here
			//this.getLogger().error('No redis connection pool for ' + poolName);
		}
	}

    get mysqlPool() {
        return this._mysqlPool;
    }

    /**
     * Setup the MySQL connection pool
     */
    setupMySQLPool() {
        var options = this.getConfiguration(Configuration.MYSQL);

        if (options) {
            this._mysqlPool = mysql.createPool(options);
        }
    }

    /**
     * Acquire MySQL client from MySQL connection pool
     * 
     * @name acquireMySQLClient
     * @param {Function} callback - The callback function
     * 
     * @returns {void}
     */
    acquireMySQLClient(callback) {
        if(this.mysqlPool) {
            this.mysqlPool.getConnection(function(error, mysql) {
                if(error) {
                    throw error;
                } else {
                    callback(null, mysql);
                }
            });
        } else {
            callback(new Error("No MySQL Connection pool."), null);
        }
    }
    
    /**
     * Release MySQL connection from connection pool
     * 
     * @name releaseMySQLClient
     * @param {Connection} connection - The MySQL connection
     * 
     * @returns {void}
     */
    releaseMySQLClient(connection) {
        if(this.mysqlPool) {
            this.mysqlPool.releaseConnection(connection);
        } else {
            //TODO: add log here
        }
    }

    get alipayAPI() {
        return this._alipayAPI;
    }

    /**
     * Setup the alipay
     */
    setupAlipay() {
        var alipayConfig = this.getConfiguration(Configuration.ALIPAY);
        this._alipayAPI = alipay;
        this._alipayAPI.config({
            seller_email: alipayConfig.seller_email,
            partner : alipayConfig.partner_id,
            key: alipayConfig.key,
            notify_url: this.getAlipayParameterURL('notify_url'),
            return_url: this.getAlipayParameterURL('return_url')
        });
    }

    /**
     * Build alipay direct payment url
     * 
     * @name buildAlipayURL
     * @param {String} orderId - The merchant's orderId
     * @param {String} subject - The order's subject
     * @param {String} body - The order's body
     * @param {String} amount - The amount need to payment
     * 
     * @returns {String} 
     */
    buildAlipayURL(orderId, subject, body, amount) {
        return this.alipayAPI.buildDirectPayURL({
            out_trade_no: orderId,
            subject: subject,
            body: body,
            total_fee: amount
        });
    }

    /**
     * Verify the notification from Alipay's server
     * 
     * @name verifyNotify
     * @param {Object} params - The params come from Alipay
     * @param {Function} callback - The callback function
     * 
     * @returns {void}
     */
    verifyNotify(params, callback) {
        this.alipayAPI.verity(params, function (err, result) {
            if (err) {
                callback({
                    success: false,
                    error: err
                });
            } else {
                if(result===true){
                    callback({
                        success: true,
                        data: true
                    });
                } else {
                    callback({
                        success: false,
                        error: result
                    })
                }
            }
        });
    }

    get qiniuClient() {
        return this._qiniu;
    }

    /**
     * Setup the qiniu client
     * 
     * @name setupQiniuClient
     * 
     * @returns {void}
     */
    setupQiniuClient() {
        var qiniuConfig = this.getConfiguration(Configuration.QINIU);
        this._qiniu = qiniu;
        this._qiniu.config({
            access_key: qiniuConfig.ak,
            secret_key: qiniuConfig.sk
        });
    }

    /**
     * Get a qiniu bucket by name
     * 
     * @name getQiniuBucket
     * @param {String} name - the bucket name
     * 
     * @returns {qiniu.Bucket}
     */
    getQiniuBucket(name) {
        var qiniuConfig = this.getConfiguration(Configuration.QINIU);
        name = name || qiniuConfig.bucket;

        if (this.qiniuClient) {
            return this.qiniuClient.bucket(name);
        }
        
        return null;
    }

    /**
     * Get TV app update information
     * 
     * @name getTVAppUpdateInfo
     * 
     * @returns {Object}
     */
    getTVAppUpdateInfo() {
        var metadataFile = this.getConfiguration(Configuration.TV_METADATA);

        if(metadataFile) {
            var metadataFile = path.resolve(this.cwd, metadataFile);
            if(fs.existsSync(metadataFile)) {
                var data = fs.readFileSync(metadataFile);
                return JSON.parse(data);
            }
        }
        return {
            "versionCode" : "~",
            "description" : "~",
            "downloadUrl" : "~"
        };
    }

    /**
     * Get alipay payment url by type
     * 
     * @name getAlipayParameterURL
     * @param {String} type - The server type
     * 
     * @returns {String}
     */
    getAlipayParameterURL(type) {
        var appConfig = this.getConfiguration(Configuration.APPLICATION);
        var url = 'http://';
        switch(appConfig.env) {
            case 'local':
                url += 'localhost' + (appConfig.port === 80 ? '' :  ':' + appConfig.port) + '/alipay/' + type;
            break;
            case 'pro':
                url += 'billboards.parllay.cn' + (appConfig.port === 80 ? '' :  ':' + appConfig.port) + '/alipay/' + type;
            break;
            case 'ppe':
                url += 'onwall-ppe.parllay.cn' + (appConfig.port === 80 ? '' :  ':' + appConfig.port) + '/alipay/' + type;
            break;
        }

        return url;
    }

    /**
     * Report error to application
     * 
     * @name reportError
     * @param {String} errorName
     */
    reportError(errorName, error) {
        this.Application.reportError(errorName, error);
    }
}

exports.ApplicationContext = ApplicationContext;